import { IRpcServerHost, ServerHostEvents } from "dream_rpc"
import { BridgeHostEvents, Completer, IRpcBridgeHost } from "dream_rpc"
/**
 * @author مۇختەرجان مەخمۇت (mutearjan mahmood)
 * @email yeganaaa@163.com`
 * @create date 2020-08-30 22:39:10
 * @modify date 2020-08-30 22:39:10
 * @desc [description]
*/

import * as Net from "net"
import {ClosingBySystemException, IClosingReason} from "dream_rpc/bin/Core/RpcBridge";

/**
* Buffered socket wrapper
* this is useful for sending and receiving data without split into multiple buffers(when send) and merge multiple buffers to single(when receive).
* for receive data, you can assign [BufferedSocketWrapper.onDataReceived] callback property.
*/
export class BufferedSocketWrapper {
    private buffer: Uint8Array = null!
    private shouldBeReadBytes: number = 0
    private receivedBytes: number = 0
    private writeCompleter: Completer<void> = null!
    public onDataReceived: (data: Uint8Array) => void = data => { }
    public constructor(public socket: Net.Socket, public bufferSize: number) {
        this.buffer = new Uint8Array(this.bufferSize)

        socket.on("error", error => {
            this.writeCompleter?.error(error)
        })
        const receive = (data: Buffer) => {
            let offset = 0
            if (this.shouldBeReadBytes == 0) {
                this.shouldBeReadBytes = data.readUInt32LE(offset)
                offset += 4
            }
            if (this.shouldBeReadBytes - this.receivedBytes <= data.length - offset) { //If received data is enough then:
                data.copy(this.buffer, this.receivedBytes, offset, (this.shouldBeReadBytes - this.receivedBytes) + offset)
                const actualData = this.buffer.slice(0, this.shouldBeReadBytes)
                offset += this.shouldBeReadBytes
                this.shouldBeReadBytes = 0
                this.receivedBytes = 0
                this.onDataReceived(actualData)
                if (offset < data.length) receive(data.slice(offset))
            }
            else {
                data.copy(this.buffer, this.receivedBytes, offset, data.length - offset) //Collect data
                this.receivedBytes += data.length - offset //And increase received bytes size.
            }
        }
        socket.on("data", buffer => receive(buffer))
        socket.on("close", () => {
            this.buffer = null!
        })
    }
    private lengthData = new Uint8Array(4)
    public async write(data: Uint8Array): Promise<void> {
        this.writeCompleter = Completer.create()
        const dataView = new DataView(this.lengthData.buffer, this.lengthData.byteOffset, this.lengthData.byteLength)
        dataView.setUint32(0, data.byteLength, true)
        let hasWritten = true
        hasWritten = hasWritten && this.socket.write(this.lengthData);
        hasWritten = hasWritten && this.socket.write(data);
        if (hasWritten) {
            this.writeCompleter.complete()
        } else {
            this.socket.once("drain", () => {
                this.writeCompleter.complete()
            })
        }
        await this.writeCompleter.promise
    }
}
enum ConnectionState {
    CLOSED = 0x01,
    OPENED = 0x02
}
export class TcpSocketBridgeHost implements IRpcBridgeHost<Net.Socket> {
    backend: Net.Socket = null!
    handler: BridgeHostEvents = null!
    private pendingToOpen = Completer.create()
    public createdByServer = false
    private wrapper: BufferedSocketWrapper = null!
    private state = ConnectionState.CLOSED
    private _beforeInitialization = true
    constructor(private creator: () => Promise<Net.Socket>, private bufferSize = 1024 * 128) {}

    public async prepare(eventHandler: BridgeHostEvents): Promise<void> {
        this.handler = eventHandler
        this.backend = await this.creator()

        this.wrapper = new BufferedSocketWrapper(this.backend, this.bufferSize)

        this.backend.on("connect", async () => {
            if (this.createdByServer) return
            await this.pendingToOpen.promise
            await this.handler.onReady()
            this.state = ConnectionState.OPENED
            this._beforeInitialization = false
        })
        this.backend.on("close", async hadError => {
            await this.pendingToOpen.promise
            if (hadError) await this.handler.onError(new Error("Closed abnormally"))
            if (this.state != ConnectionState.CLOSED) {
                this.state = ConnectionState.CLOSED
                await this.handler.onClose()
            }
        })
        this.wrapper.onDataReceived = async data => {
            await this.pendingToOpen.promise
            await this.handler.onMessage(data)
        }
        this.backend.on("error", async error => {
            await this.pendingToOpen.promise
            await this.handler.onError(error)
            await this.close(new ClosingBySystemException(error))
        })
        if (this.createdByServer) (async () => {
            await this.pendingToOpen.promise
            this.handler.onReady()
            this.state = ConnectionState.OPENED
        })()
    }
    public async open(): Promise<void> {
        this.pendingToOpen.complete()
    }
    public async close(reason: IClosingReason): Promise<void> {
        this.backend.destroy()
        if (this._beforeInitialization) await this.handler.onUnableToConnect();

        if (this.state != ConnectionState.CLOSED) {
            this.state = ConnectionState.CLOSED
            await this.handler.onClose()
        }
    }
    public async send(data: Uint8Array): Promise<void> {
        await this.wrapper.write(data)
    }
}
export class TcpSocketServerHost implements IRpcServerHost<Net.Server> {
    backend: Net.Server = null!
    handler: ServerHostEvents = null!
    public constructor(private creator: () => Promise<Net.Server>, private launchAction: (server: Net.Server) => Promise<void>) {}
    public async prepare(eventHandler: ServerHostEvents): Promise<void> {
        this.handler = eventHandler;
        this.backend = await this.creator()
        this.backend.on("listening", async () => {
            await this.handler.onReady()
        })
        this.backend.on("error", async error => {
            await this.handler.onError(error)
        })
        this.backend.on("connection", async socket => {
            const clientHost = new TcpSocketBridgeHost(async () => socket)
            clientHost.createdByServer = true
            await this.handler.onClientConnected(clientHost)
        })
        this.backend.on("close", async () => {
            await this.handler.onShutdown()
        })
    }
    public async launch(): Promise<void> {
        await this.launchAction(this.backend)
    }
    public async shutdown(): Promise<void> {
        const completer = Completer.create()
        this.backend.close((error) => {
            if (error == null) completer.complete()
            else completer.error(error)
        })
        await completer.promise
    }
}